package com.xrh.xdemo.service.aspects;

import java.lang.reflect.Method;
import java.util.Date;
import java.util.concurrent.TimeUnit;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import com.xrh.core.annotation.RepeatRequest;
import com.xrh.core.exceptions.BusinessException;

import cn.hutool.cache.impl.TimedCache;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import lombok.extern.slf4j.Slf4j;


/**
 * @function 拦截注解RepeatRequest声明的方法
 * @author 肖荣辉
 * @date 2023年12月21日
*/
@Aspect
@Slf4j
@Component
public class RepeatRequestAspect {

    //默认缓存5分钟
    private static TimedCache<String, String> localCache = new TimedCache<>(5 * 60 * 1000);
    
    //当前线程缓存
    private ThreadLocal<String> keyThreadLocal = new ThreadLocal<String>();

    static {
        //这个缓存 , 如果不去get它是不会自动清理的, 设置1个小时清理一次已经失效的
        localCache.schedulePrune(60 * 60 * 1000);
    }

    @Before("@annotation(repeatRequest)")
    public void beforePointCut(JoinPoint joinPoint, RepeatRequest repeatRequest) {
    	
        // 解析 Key
        String key = createKey(joinPoint, repeatRequest);

        //如果key不暂存在则把key设置到缓存中
        String oldValue = setIfNotExist(key, repeatRequest.timeout(), repeatRequest.timeUnit());
        
        //key存在 , 则证明在规定的timeout内, 重复调用了2次
        if (oldValue != null) {
            log.info("[beforePointCut][方法({}) 参数({}) 存在重复请求]，上次请求时间{}", joinPoint.getSignature().toString(), joinPoint.getArgs(), oldValue);
            throw new BusinessException(repeatRequest.message());
        } else {
            //把本地生成的key放到线程缓存里头,等切面结束的时候用来删除缓存中的key
            keyThreadLocal.set(key);
        }
        
    }

    /**
	 * @function 如果不存在key，则设置值
	 * @author 肖荣辉
	 * @date 2023年12月21日
	*/
    private synchronized String setIfNotExist(String key, int timeout, TimeUnit timeUnit) {
    	
        String now = DatePattern.NORM_DATETIME_MS_FORMAT.format(new Date());
        
        //从缓存中获得对象，当对象不在缓存中或已经过期返回null 
		//调用此方法时，会检查上次调用时间，如果与当前时间差值大于超时时间返回null，否则返回值。 
		//每次调用此方法会可选是否刷新最后访问时间，true表示会重新计算超时时间。
        String value = localCache.get(key, false);
        
        if (value != null) {
            return value;
        }
        
        localCache.put(key, now, timeUnit.toMillis(timeout));
        
        return null;
    }


    @After("@annotation(repeatRequest)")
    public void afterPointCut(JoinPoint joinPoint, RepeatRequest repeatRequest) {
    	
    	//取出切面进入时生成的key
        String key = keyThreadLocal.get();
        
        //从缓存中移除key
        if (key != null && repeatRequest.delKey()) {
            localCache.remove(key);
            log.info("[idempotent]:has removed key={}", key);
        }
        
        keyThreadLocal.remove();
    }
    
    /**
	 * @function 根据请求中的参数生成一个md5的key, 用于判断是否重复请求
	 * @author 肖荣辉
	 * @date 2023年12月21日
	*/
    public String createKey(JoinPoint joinPoint, RepeatRequest repeatRequest) {
        Method method = getMethod(joinPoint);
        String className = method.getDeclaringClass().getSimpleName();
        String methodName = method.getName();
        //joinPoint.getArgs()返回的是对象的参数拼接 , 能满足重复请求的判定
        String argsStr = StrUtil.join(",", joinPoint.getArgs());
        return SecureUtil.md5(className + methodName + argsStr);
    }

    /**
	 * @function 获取方法
	 * @author 肖荣辉
	 * @date 2023年12月21日
	*/
    public Method getMethod(JoinPoint point) {
    	
        // 处理，声明在类上的情况
        MethodSignature signature = (MethodSignature) point.getSignature();
        
        Method method = signature.getMethod();
        
        if (!method.getDeclaringClass().isInterface()) {
            return method;
        }

        // 处理，声明在接口上的情况
        try {
            return point.getTarget().getClass().getDeclaredMethod( point.getSignature().getName(), method.getParameterTypes());
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
        
    }
    
}
